03. Prompt Engineering 与 Context Engineering
本章高频面试题
- 什么是 Prompt Engineering?什么是 Context Engineering?两者有什么区别?
- 为什么很多 Agent 的问题不是 Prompt 写得差,而是 Context 设计得差?
- System Prompt、Developer Prompt、User Prompt 应该如何分层?
- 什么是动态 Prompt?什么时候该动态注入上下文?
- 为什么”把所有信息都塞给模型”通常不是最优解?长上下文为什么会退化?
- 长上下文下,如何避免信息污染、目标偏移和召回失真?什么是 context rot?
- Prompt Caching 怎么设计才能真正命中?Anthropic 和 OpenAI 的 cache 行为有什么差别?
- Prompt 工程的最佳实践有哪些?Context 工程的最佳实践有哪些?
- 结构化输出应该用 JSON mode、tool use 还是 constrained decoding?
- 什么应该通过 Prompt 解决,什么必须通过系统设计解决?
1. 什么是 Prompt Engineering
Prompt Engineering 是指为了让模型更稳定地产出符合要求的结果,而去设计输入指令、示例、输出约束和消息结构的过程。
简单说,它解决的是:
如何把任务讲清楚,让模型更容易正确执行。
Prompt 工程通常会涉及这些内容:
- 角色设定
- 任务说明
- 约束条件
- 输出格式
- few-shot 示例
- 工具调用说明
2. 什么是 Context Engineering
Context Engineering 比 Prompt 更广。
它不是只关心”写什么提示词”,而是关心:
在某一次模型调用前,究竟应该把哪些信息、以什么格式、在什么时机、放进模型上下文里。
这包括:
- system prompt
- 当前任务状态
- 历史消息
- 检索结果
- 工具列表
- 用户偏好
- 权限信息
- 环境配置
- 输出 schema
你可以把两者理解成:
- Prompt Engineering:如何写说明书
- Context Engineering:如何准备整张作战桌面
3. 为什么 Agent 的核心更接近 Context Engineering
LangChain 官方文档把这点讲得非常明确:
Agent 不可靠,很多时候不是因为模型能力不够,而是因为没有把”正确的上下文”传给模型。
如果你只是问模型一个简单问题,Prompt 往往足够。 但在 Agent 场景下,模型每轮需要看到的不只是文本说明,还包括:
- 当前目标
- 已完成步骤
- 可用工具
- 上一步工具结果
- 用户权限
- 需要遵守的边界
所以 Agent 工程本质上非常依赖 Context Engineering。
4. Prompt 分层:System / Developer / User
4.1 System Prompt
它主要定义稳定、长期有效的高优先级规则,比如:
- 你是谁
- 你能做什么
- 你不能做什么
- 风格和安全边界
System prompt 是最适合做 prompt caching 的位置(见 §8),所以应该尽量保持稳定、避免每次请求都改。
4.2 Developer Prompt
它主要承载应用开发者定义的任务策略和格式规范,例如:
- 回答要包含引用
- 工具调用顺序建议
- 输出 JSON 或 Markdown 结构
- 特定业务规则
4.3 User Prompt
它是用户当前这一轮提出的任务目标。
最常见的错误是把所有内容都塞到 User Prompt。 更好的做法是把不同层级的约束拆开。
4.4 结构化标签:XML vs Markdown
一个容易忽略的细节:不同模型偏好的结构化标签不一样。
- Claude 系列在官方 prompting guide 中明确推荐 XML 标签,例如
<instructions>...</instructions>、<context>...</context>、<example>...</example> - OpenAI 系列对 Markdown 标题和列表友好
- 开源模型通常对清晰的分节 header 敏感
如果你在多模型路由,建议统一抽象成”section → 具体渲染”的模式,由 adapter 层按目标模型转成 XML / Markdown / 其他。
5. Prompt 工程的核心实践
5.1 把目标、约束、成功标准说清楚
差的 Prompt:
“帮我分析一下这个需求。”
更好的 Prompt:
“请把这个需求拆成目标、输入输出、边界条件、风险点和待确认问题五部分。如果信息不足,请先列出缺失信息,不要直接假设实现方案。“
5.2 用结构帮助模型理解层次
# 角色
你是一个资深后端工程师。
# 目标
设计一个订单退款 Agent 的工具调用策略。
# 约束
- 不允许直接退款
- 必须先检查订单状态
- 涉及资金变更必须人工确认
# 输出格式
请输出:
1. 工具清单
2. 调用顺序
3. 风险控制点5.3 用 few-shot 教”模式”,不用 few-shot 塞”知识”
few-shot 最适合演示:
- 任务的模式
- 输出的风格
- 正确和错误样例
不要把 few-shot 当成知识库替代品。 知识应该通过检索或数据库传入,而不是靠示例硬塞。
5.4 Prompt 要版本化,且和 eval 一起迭代
Prompt 是应用逻辑的一部分。 如果不版本化,就很难知道:
- 哪个版本效果更好
- 什么时候改坏了
- 模型升级后是不是 prompt 不再适配
实践上:
- Prompt 文件放代码仓,打版本号(语义化或 hash)
- 每次 prompt 变更必须跑回归评估集(见第 9 章)
- Trace metadata 里带
prompt_version,线上问题能定位到版本 - 模型升级(Opus 4.6 → 4.7)当作独立的 prompt 重评事件处理
6. TypeScript 示例:把 Prompt 模板结构化
type PromptContext = {
userRole: "viewer" | "operator" | "admin";
task: string;
constraints: string[];
outputFormat: "markdown" | "json";
};
export function buildDeveloperPrompt(ctx: PromptContext): string {
const roleRules =
ctx.userRole === "admin"
? "当前用户具备管理员权限,但高风险操作仍需确认。"
: ctx.userRole === "operator"
? "当前用户具备操作权限,只允许低风险写操作。"
: "当前用户为只读权限,只允许检索和分析。";
return [
"# 角色",
"你是一个可靠的 Agent 执行器。",
"",
"# 权限",
roleRules,
"",
"# 当前任务",
ctx.task,
"",
"# 约束",
...ctx.constraints.map((item) => `- ${item}`),
"",
"# 输出要求",
ctx.outputFormat === "json"
? "最终输出必须符合预定义 JSON Schema。"
: "最终输出使用 Markdown,必须包含结论、依据和风险。",
].join("\n");
}这里的重点不是”字符串拼接”本身,而是:
- Prompt 输入被结构化了
- 各种约束被显式建模了
- 后续更容易做测试和版本管理
7. Context Engineering 的三层视角
LangChain 官方文档把 Agent 的 context 拆成三类,很值得记住。
7.1 Model Context
某一次模型调用时,真正喂给模型的上下文内容:
- instructions
- messages
- tools
- output format
- 当前选择的模型
7.2 Tool Context
工具在执行时能读取和写入什么:
- 当前 state
- runtime context
- store
- 权限
- 外部连接
7.3 Life-cycle Context
在模型调用和工具调用之间系统还会做什么:
- 历史消息压缩
- guardrails
- logging
- summarization
- state 更新
这个分类提醒你:
Agent 可靠性不只是 prompt 内容问题,而是整个调用生命周期的问题。
8. Prompt Caching:工程上最值钱的优化之一
Prompt caching 是 2025-2026 生产 Agent 里最容易被低估的成本杠杆。以 Anthropic 为例:cache 写入是基础输入价的 1.25x(5 分钟 TTL)或 2x(1 小时 TTL),但 cache 读只要 0.1x。稳定的 system prompt + tool schema + 长参考文档前缀被反复命中时,成本能直接砍到十分之一,TTFB 也明显下降。
8.1 Anthropic 的 cache_control 语义
Anthropic 用显式 cache_control 标记缓存断点:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-4-7",
max_tokens: 1024,
system: [
{
type: "text",
text: LARGE_SYSTEM_PROMPT,
cache_control: { type: "ephemeral" },
},
],
tools: [
/* ... */
],
messages: [
{ role: "user", content: "..." },
{
role: "assistant",
content: "...",
// 在会话快照处打一个 breakpoint
cache_control: { type: "ephemeral" },
},
{ role: "user", content: "新的问题" },
],
});
console.log(response.usage);
// cache_creation_input_tokens / cache_read_input_tokens / input_tokens几条关键规则(对照 Anthropic 官方文档,2026-04 状态):
- 每个请求最多 4 个 cache breakpoint
- 默认 TTL 5 分钟,可选
ttl: "1h"换 2x 写入成本 - 最小缓存长度:Claude Opus 4.7/4.6/4.5 和 Haiku 4.5 是 4096 tokens,Sonnet 系列 2048,低于阈值会静默不缓存
- Cache 读不计入 rate limit
cache_control之前的所有内容都会被缓存,所以顺序是 system prompt → tools → 历史消息,越稳定的放越前面
8.2 让 cache 真正命中的工程规矩
- 前缀稳定:凡是放在 system prompt 和 tools 的内容,任何字段的改动(哪怕重排)都会让缓存失效
- 动态部分后置:用户输入、当前时间戳、workspace 特定上下文放在 messages 后段
- Tool schema 分离:高频迭代的 description 和稳定的 schema 分层管理,schema 变更打版本
- 监控命中率:
cache_read_input_tokens / (cache_read + cache_creation + input)是核心 KPI,低于 50% 说明架构有问题 - 小心时间戳:system prompt 里写”今天是 {now}“会让前缀每秒变一次,直接毁掉缓存
OpenAI 的 prompt caching 是自动的(无需显式标记),但同样依赖前缀稳定性,规矩完全类似。
9. 为什么”把所有信息都塞给模型”通常不是最优解
很多初学者有一个直觉:
“只要给模型更多信息,它就会更准。”
这在实际中经常不成立。几个已经被反复验证的问题:
9.1 Lost in the Middle
2023 年 Stanford 等团队的研究明确指出:模型对上下文中间位置的信息召回精度显著低于头尾。这个现象在 2024-2025 的长上下文模型上仍然存在(虽然有所缓解)。
工程含义:重要信息放头放尾,不要埋在 50k tokens 中间。
9.2 Context Rot
长上下文会随对话进行逐步退化——哪怕窗口没满,模型的任务遵循能力和工具选择准确率也会下降。这在 2024-2025 多个团队的评测里被反复观察到(Anthropic、Chroma 等都发过相关报告)。
工程含义:定期 compact,不要追求”把一切塞进窗口”。
9.3 注意力稀释
历史信息和当前目标混杂会导致注意力漂移,检索召回的片段太多会让真正关键的证据不突出。
所以 Context Engineering 的关键不是”更多”,而是”更对”。
10. Context Engineering 的推荐做法
10.1 分层组织上下文
比较稳的做法是把上下文拆成几个槽位:
- 稳定规则(system prompt)
- 工具/skill 清单(catalog)
- 当前任务状态(显式 state)
- 最近对话窗口
- 检索结果(带引用元数据)
- 权限和环境
不要把这些内容混成一大段自然语言。
10.2 只给”此刻有用的上下文”
例如:
- 当前在写邮件,就不需要把全部工具说明都给模型
- 当前在审批退款,就应该把权限、订单状态和审批规则显式注入
- 当前在检索问答,就应重点注入证据片段和引用格式要求
10.3 动态注入上下文
不同轮次、不同用户、不同阶段,模型看到的内容可以不同。
LangChain 用 dynamicSystemPromptMiddleware 承载这个思路:
import { z } from "zod";
import { createAgent, dynamicSystemPromptMiddleware } from "langchain";
const contextSchema = z.object({
userRole: z.enum(["viewer", "operator", "admin"]),
});
type RuntimeContext = z.infer<typeof contextSchema>;
const agent = createAgent({
model: "anthropic:claude-opus-4-7",
tools: [],
contextSchema,
middleware: [
dynamicSystemPromptMiddleware<RuntimeContext>((state, runtime) => {
let prompt = "你是一个可靠的企业助手。";
if (runtime.context.userRole === "viewer") {
prompt += "\n当前用户只读,禁止建议执行写操作。";
}
if (state.messages.length > 10) {
prompt += "\n当前对话较长,请保持回答简洁,只突出关键结论。";
}
return prompt;
}),
],
});新版 LangChain 的 middleware 还提供 wrapModelCall、wrapToolCall、beforeModel、afterModel 等 hook,你可以在模型调用前做消息裁剪、权限过滤、工具可见性动态控制;在调用后做结果审查。createMiddleware({...}) 把这些拼成可组合的单位。
10.4 对长历史做压缩和选择(Compaction)
不要默认把整段历史原样传给模型。常用手段:
- 滑动窗口:只保留最近 N 轮
- 阶段性摘要:每 M 轮或每个 milestone 触发一次 LLM summary,把细节压缩成结构化要点
- Semantic checkpoint:在任务关键节点(计划确认、审批通过、重大结果)生成 checkpoint summary,之后的对话基于此 checkpoint
- Scratchpad offload:把中间推理和工具调用结果写到外部 state/文件系统,而不是全塞在 message history 里(这是 Claude Code、Deep Agents 等系统的核心模式)
- Hybrid retrieval over history:把历史消息本身向量化,按需检索相关片段
一个实用原则:超过窗口 50% 就应该考虑 compact。等满了再做是被动的,会有 token 浪费和延迟尖峰。
11. 结构化输出:JSON mode vs Tool use vs Constrained decoding
这是一个面试常问、工程常踩坑的点。
11.1 几种方式
- Prompt 里要求 JSON:最不稳定,依赖模型”听话”
- Provider 的 JSON mode:OpenAI 的
response_format: { type: "json_object" }、Anthropic 靠 system prompt + tool use。保证”是合法 JSON”,不保证”符合某个 schema” - Structured Outputs / 强 schema:OpenAI 的
response_format: { type: "json_schema", ... strict: true }、Anthropic 通过 tool use 强制 schema。这是当前生产推荐 - Tool use 作为伪装的结构化输出:把”输出结构化结果”当成一次 tool call,schema 是工具的 input schema
- Constrained decoding / grammar:推理栈支持时(vLLM、llama.cpp、Outlines),token 级强制符合 grammar,是最硬的约束
11.2 工程选择原则
- 能用 provider 的 strict schema 就用。OpenAI 的
strict: true和 Anthropic 的 tool use 现在都够稳 - Tool use 是天然的结构化输出机制。如果你的 agent 反正要 tool call,最终结构化答案本身就是一次 tool call,schema 复用就行
- Open-ended 生成配合”先结构后正文”。例如先输出
{ "plan": [...], "needs_tool": true },再生成自然语言 - 记得防御解析失败。即使是 strict,网络错误、truncation、模型降级都可能让 JSON 坏掉——必须有一层 robust 解析 + 重试
12. 什么应该靠 Prompt 解决,什么必须靠系统解决
更适合靠 Prompt 的问题
- 输出风格
- 结构化表达
- 任务说明
- 审慎回答策略
- 轻量格式要求
更适合靠系统解决的问题
- 权限控制(server-side 校验)
- 工具可见性(tool filtering)
- 上下文裁剪(middleware)
- 长期记忆管理(存储层 + 写入策略)
- 错误恢复(runtime)
- 事件流(event store)
- 结果校验(schema + validator)
- 人工审批(interrupt)
一个很好记的原则:
Prompt 负责”表达意图”,系统负责”保证边界和稳定性”。
13. 本章方法论小结
- Prompt Engineering 解决”怎么把任务讲清楚”
- Context Engineering 解决”模型这一轮究竟该看到什么”
- Agent 的可靠性问题,很多时候更像 Context 问题而不是 Prompt 问题
- Prompt 要分层、要版本化、要和 eval 一起迭代
- Prompt caching 是成本杠杆:稳定前缀 + 动态后置 + 命中率监控
- 上下文不是越多越好,而是越对越好——警惕 lost in the middle 和 context rot
- 动态上下文、分层注入、压缩与检索、scratchpad offload,是生产系统的关键能力
- 结构化输出优先用 provider 的 strict schema 或 tool use,不要依赖”prompt 里请求 JSON”
- Prompt 不能替代权限、状态机、校验和恢复机制